查看原文
其他

设计模式系列1 - 模板模式&策略模式

吕梦楼 楼仔 2022-10-28

分别讲述模板模式和策略模式的使用姿势,以及两者的区别,基于java。

楼哥,天天看你写文章,感觉好无聊,要不先给我们讲个笑话吧。“嗯,讲什么呢?我先想想...”

有个记者去南极采访一群企鹅,他问第一只企鹅:“你每天都干什么?”

企鹅说:“吃饭,睡觉,打豆豆!”

接着又问第2只企鹅,那只企鹅还是说:“吃饭,睡觉,打豆豆!”

记者带着困惑问其他的企鹅,答案都一样,就这样一直问了99只企鹅。

当走到第100只小企鹅旁边时,记者走过去问它:每天都做些什么啊?

那只小企鹅回答:"吃饭,睡觉."

记者惊奇的又问:"你怎么不打豆豆?"

小企鹅撇着嘴巴,瞪了记者一眼说:"我就是豆豆!"

刚看到这个笑话时,感觉既简单又好笑,于是就有了我后面这篇文章的灵感。

最Low方式

假如现在有3只企鹅,都喜欢“吃饭,睡觉,打豆豆”:

public class littlePenguin {
    public void everyDay() {
        System.out.println("吃饭");
        System.out.println("睡觉");
        System.out.println("用小翅膀打豆豆");
    }
}
public class middlePenguin {
    public void everyDay() {
        System.out.println("吃饭");
        System.out.println("睡觉");
        System.out.println("用圆圆的肚子打豆豆");
    }
}
public class bigPenguin {
    public void everyDay() {
        System.out.println("吃饭");
        System.out.println("睡觉");
        System.out.println("拿鸡毛掸子打豆豆");
    }
}
public class test {
    public static void main(String[] args) {
        System.out.println("littlePenguin:");
        littlePenguin penguin_1 = new littlePenguin();
        penguin_1.everyDay();
        
        System.out.println("middlePenguin:");
        middlePenguin penguin_2 = new middlePenguin();
        penguin_2.everyDay();
        
        System.out.println("bigPenguin:");
        bigPenguin penguin_3 = new bigPenguin();
        penguin_3.everyDay();
    }
}

看一下执行结果:

littlePenguin:
吃饭
睡觉
用小翅膀打豆豆
middlePenguin:
吃饭
睡觉
用圆圆的肚子打豆豆
bigPenguin:
吃饭
睡觉
拿鸡毛掸子打豆豆

这种方式是大家写代码时,最容易使用的方式,上手简单,也容易理解,目前看项目中陈旧的代码,经常能找到它们的影子,下面我们看怎么一步步将其进行重构。

常规方式

“吃饭,睡觉,打豆豆”其实都是独立的行为,为了不相互影响(比如吃饭时突然睡着了,或者睡觉时不好好睡,居然急着跑去打豆豆,开个玩笑哈~~),我们可以通过函数简单进行封装:

public class littlePenguin {
    public void eating() {
        System.out.println("吃饭");
    }
    public void sleeping() {
        System.out.println("睡觉");
    }
    public void beating() {
        System.out.println("用小翅膀打豆豆");
    }
}
public class middlePenguin {
    public void eating() {
        System.out.println("吃饭");
    }
    public void sleeping() {
        System.out.println("睡觉");
    }
    public void beating() {
        System.out.println("用圆圆的肚子打豆豆");
    }
}
// bigPenguin相同,省略...
public class test {
    public static void main(String[] args) {
        System.out.println("littlePenguin:");
        littlePenguin penguin_1 = new littlePenguin();
        penguin_1.eating();
        penguin_1.sleeping();
        penguin_1.beating();
        // 下同,省略...
    }
}

这样看起来,是不是要稍微清晰一些呢,工作过一段时间的同学,可能会采用这种实现方式,我们有没有更优雅的实现方式呢?

模板模式

在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。

这3只企鹅,由于每天吃的都一样,睡觉也都是站着睡,但是打豆豆的方式却不同,所以我们可以将“吃饭,睡觉,打豆豆”抽象出来,因为“吃饭,睡觉”都一样,所以我们可以直接实现出来,但是他们“打豆豆”的方式不同,所以封装成抽象方法,需要每个企鹅单独去实现“打豆豆”的方式。最后再新增一个方法everyDay(),固定每天的执行流程:

public abstract class penguin {
    public void eating() {
        System.out.println("吃饭");
    }
    public void sleeping() {
        System.out.println("睡觉");
    }
    public abstract void beating();
    public void everyDay() {
        this.eating();
        this.sleeping();
        this.beating();
    }
}

每只企鹅单独实现自己“打豆豆”的方式:

public class littlePenguin extends penguin {
    @Override
    public void beating() {
        System.out.println("用小翅膀打豆豆");
    }
}
public class middlePenguin extends penguin {
    @Override
    public void beating() {
        System.out.println("用圆圆的肚子打豆豆");
    }
}
public class bigPenguin extends penguin {
    @Override
    public void beating() {
        System.out.println("拿鸡毛掸子打豆豆");
    }
}

最后看调用方式:

public class test {
    public static void main(String[] args) {
        System.out.println("littlePenguin:");
        littlePenguin penguin1 = new littlePenguin();
        penguin1.everyDay();
        System.out.println("middlePenguin:");
        middlePenguin penguin2 = new middlePenguin();
        penguin2.everyDay();
        System.out.println("bigPenguin:");
        bigPenguin penguin3 = new bigPenguin();
        penguin3.everyDay();
    }
}

“楼哥,你这代码看的费劲,能给我画一个UML图么”,“嗯,其实画图挺麻烦的,谁让楼哥是暖男呢,那我就学着给大家画一个”

策略模式

在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。

我们还是先抽象出3个企鹅的行为:

public abstract class penguin {
    public void eating() {
        System.out.println("吃饭");
    }
    public void sleeping() {
        System.out.println("睡觉");
    }
    public abstract void beating();
}

每只企鹅单独实现自己“打豆豆”的方式:

public class littlePenguin extends penguin {
    @Override
    public void beating() {
        System.out.println("用小翅膀打豆豆");
    }
}
public class middlePenguin extends penguin {
    @Override
    public void beating() {
        System.out.println("用圆圆的肚子打豆豆");
    }
}
public class bigPenguin extends penguin {
    @Override
    public void beating() {
        System.out.println("拿鸡毛掸子打豆豆");
    }
}

这里就是策略模式的重点,我们再看一下策略模式的定义“我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的context对象”,那么该contex对象如下:

public class behaviorContext {
    private penguin _penguin;

    public behaviorContext(penguin newPenguin) {
        _penguin = newPenguin;
    }
    public void setPenguin(penguin newPenguin) {
        _penguin = newPenguin;
    }
    public void everyDay() {
        _penguin.eating();
        _penguin.sleeping();
        _penguin.beating();
    }
}

最后看调用方式:

public class test {
    public static void main(String[] args) {
        behaviorContext behavior = new behaviorContext(new littlePenguin());
        behavior.everyDay();

        behavior.setPenguin(new middlePenguin());
        behavior.everyDay();

        behavior.setPenguin(new bigPenguin());
        behavior.everyDay();
    }
}

我们可以通过给behaviorContext传递不同的对象,然后来约定everyDay()的调用方式。其实我这个示例,有点把策略模式讲复杂了,因为纯粹的策略模式,3个企鹅只有beating()方法不同,所以可以把beating()理解为不同的算法即可,之所以引入everyDay(),是因为实际的项目场景中,会经常这么使用,也就是把这个变化的算法beating(),包装到具体的执行流程里面,所以策略模式就看起来没有那么直观,但是核心思想是一样的。

再强调一下,下面的UML图是使用了策略模式,因为我想结合具体的业务场景去讲,如果大家想去看最简版的策略模式,那就没有everyDay的封装,只有对beating执行的策略变更,这个可以看一下菜鸟教程。

模板模式 vs 策略模式

我在选择模板模式和策略模式时,发现两者都可以完全满足我的需求,然后我到网上查阅了很多资料,希望能找到两种模式在技术选择时,能确定告诉我哪些情况需要选择哪种模式,说来惭愧,到现在我都没有找到,因为网上只告诉我两种实现姿势的区别,但是没有说明如何具体选型,下面我就把我收集的资料,觉得比较核心的部分列出来,给大家一些参考。

有人请向我解释模板方法模式和策略模式之间有什么区别?

据我可以告诉他们是99%相同 – 唯一的区别是模板方法模式具有抽象类作为基类,而战略类使用由每个具体战略类实现的接口。

然而,就客户而言,他们的消费方式完全一样 – 这是正确的吗?

两者的主要区别在于具体algorithm的select。

使用Template方法模式时,通过子类化模板在编译时发生。每个子类通过实现模板的抽象方法提供了一个不同的具体algorithm。当客户端调用模板的外部接口的方法时,模板根据需要调用其抽象方法(其内部接口)来调用algorithm。

相比之下, 策略模式允许在运行时通过遏制来selectalgorithm。具体algorithm是通过单独的类或函数实现的,这些类或函数作为parameter passing给构造函数或构造方法。为此参数select哪种algorithm会根据程序的状态或inputdynamic变化。

综上所述:

  • 模板方法模式:通过子类化 编译时间algorithmselect
  • 策略模式:通过遏制 运行时algorithmselect

上面是完全摘抄网上的区别说明,只看到实现姿势的区别,但是如果通过这个就能指导我去选型,我觉得还不够,下面这个可能会讲的更具体一点:

相似:

  • 策略和模板方法模式都可以用来满足开闭原则,使得软件模块在不改变代码的情况下易于扩展。

  • 两种模式都表示通用function与该function的详细实现的分离。不过,它们所提供的粒度有一些差异。

差异:

  • 在策略中,客户和策略之间的耦合更加松散,而在模板方法中,两个模块耦合得更紧密。

  • 在策略中,虽然抽象类也可以根据具体情况而使用,但大多使用一个接口,而不使用具体类,而在Template方法中大多使用抽象类或具体类,不使用接口。

  • 在Strategy模式中,类的整体行为一般用接口表示,另一方面,Template方法用于减less代码重复,样板代码在基本框架或抽象类中定义。在Template Method中,甚至可以有一个具有默认实现的具体类。

  • 简而言之,您可以在策略模式中更改整个策略(algorithm),但是在Template模式中,只有一些事情发生变化(algorithm的一部分),而其余事件保持不变。在Template Method中,不变步骤是在一个抽象基类中实现的,而变体步骤要么是默认的实现,要么根本就没有实现。在Template方法中,组件devise器强制执行algorithm所需的步骤和步骤的sorting,但允许组件客户端扩展或replace某些步骤。

看到上面的总结,感觉还是没有解答我的疑问,最后再引用一段网上的区别解读:

模板模式:

  • 它基于inheritance。

  • 定义不能被子类改变的algorithm的骨架。只有某些操作可以在子类中重写。

  • 父类完全控制algorithm ,仅将具体的步骤与具体的类进行区分。

  • 绑定是在编译时完成的。

策略模式:

  • 它基于授权/组成。

  • 它通过修改方法的行为来改变对象的内容。

  • 它用于在algorithm族之间切换。

  • 它在运行时通过在运行时用其他algorithm完全replace一个algorithm来改变对象的行为。

  • 绑定在运行时完成。

对于有强迫症的我,没有找到问题的根源,总感觉哪里不对劲,我就说一下我对于两者区别的理解吧。说实话,两种设计模式,我也就看到在实现姿势上有所区别,至于说的策略模式要定义统一接口,模板模式不这样做等,我不太赞同,因为我有时也会给模板模式定义一个通用接口。然后也有人说,策略模式需要定义一堆对象,模板模式就不需要,如果有10个不同的企鹅,模板模式不也是需要定义10个不同的企鹅类,然后再专门针对特定的方法去实现么?

所以说,这两种设计模式,我感觉还没有到非此即彼的划分,我就是怎么爽就怎么用,比如我不需要固定的执行流程,比如只去打豆豆,只需要对一个方法做具体抽象,我愿意选择策略模式,因为这个我感觉会让我需要使用的对象,更清晰一些。如果我有固定的执行流程,比如“吃饭、睡觉、打豆豆”,我更愿意使用模板方法,可能是代码看多了,也看习惯了,更愿意用模板方法去规范代码固定的执行流程。

当然,我也可以将两者结合起来使用,比如我们可以用模板方法,去实现这3只企鹅,但是对于middlePenguin,可能有分为企鹅少年A、企鹅少年B、企鹅少年C,他们都喜欢隔壁的企鹅妹妹,但是喜欢的方式不同,有暗恋的,有直接表白的,还有霸道总裁的,我可以用策略模式,去指定他们对企鹅妹妹的表达方式。

哥就是这么任性,自己怎么用的爽,就怎么来~~

实际场景

任何模式,都需要结合实际的场景来讲,才能更清晰。这两个模式,可以在你之前做过的项目中,只要稍微留意一下,应该会发现它们其实是大量存在的,比如很多框架代码,里面有很多固定的执行流程,有些逻辑是可以采用默认处理的方式,有些逻辑需要下游自己去实现,然后有些逻辑还需要提前预留钩子,比如在执行process()流程时,可能需要进行preProcess()的操作,那么这个preProcess()就是你预留的钩子,下游可以实现,也可以不实现。

所以看完这篇文章,大家可以静下心来想想,自己之前做过的项目中,有哪些用到这两种模式,然后自己再结合具体的场景总结一下,我想你应该会对这两个模式,有更深入的理解。

后记

之前一直做业务,写代码基本也都是if...else,设计模式虽然很早就知道(研究生期间把4人帮的那本《设计模式》都看了3遍),但是没有实际去写这块代码,所有总有一种雾里探花的感觉,然后时间一长,有的模式就忘记了,真遇到代码需要去重构时,或者看别人代码,还得查一下资料,“哦,原来是用的这个设计模式,对上号了”。所以这次我打算结合具体的业务场景,将常用的设计模式全部整理出来,主要是不想再眼高手低,以后代码重构时,各种设计模式能信手拈来,那么我的目的就达成了哈。

欢迎大家多多点赞,更多文章,请关注微信公众号“楼仔进阶之路”,点关注,不迷路~~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存